<?php defined('BASEPATH') OR exit('No direct script access allowed');
/**
 * CodeIgniter Redis
 *
 * A CodeIgniter library to interact with Redis
 *
 * @package        	CodeIgniter
 * @category    	Libraries
 * @author        	Joël Cox
 * @version			v0.4
 * @link 			https://github.com/joelcox/codeigniter-redis
 * @link			http://joelcox.nl
 * @license			http://www.opensource.org/licenses/mit-license.html
 */
class Redis{
    /**
     * CI
     *
     * CodeIgniter instance
     * @var 	object
     */
    private $_ci;
    /**
     * Connection
     *
     * Socket handle to the Redis server
     * @var		handle
     */
    private $_connection;
    /**
     * Debug
     *
     * Whether we're in debug mode
     * @var		bool
     */
    public $debug = FALSE;
    /**
     * CRLF
     *
     * User to delimiter arguments in the Redis unified request protocol
     * @var		string
     */
    const CRLF = "\r\n";
    /**
     * Constructor
     */
    public function __construct($params = array())
    {
        log_message('debug', 'Redis Class Initialized');
        $this->_ci = get_instance();
        $this->_ci->load->config('redis');
        // Check for the different styles of configs
        if (isset($params['connection_group']))
        {
            // Specific connection group
            $config = $this->_ci->config->item('redis_' . $params['connection_group']);
        }
        elseif (is_array($this->_ci->config->item('redis_default')))
        {
            // Default connection group
            $config = $this->_ci->config->item('redis_default');
        }
        else
        {
            // Original config style
            $config = array(
                'host' => $this->_ci->config->item('redis_host'),
                'port' => $this->_ci->config->item('redis_port'),
                'password' => $this->_ci->config->item('redis_password'),
            );
        }
        // Connect to Redis
        $this->_connection = @fsockopen($config['host'], $config['port'], $errno, $errstr, 3);
        // Display an error message if connection failed
        if ( ! $this->_connection)
        {
            show_error('Could not connect to Redis at ' . $config['host'] . ':' . $config['port']);
        }
        // Authenticate when needed
        $this->_auth($config['password']);
    }
    /**
     * Call
     *
     * Catches all undefined methods
     * @param	string	method that was called
     * @param	mixed	arguments that were passed
     * @return 	mixed
     */
    public function __call($method, $arguments)
    {
        $request = $this->_encode_request($method, $arguments);
        return $this->_write_request($request);
    }
    /**
     * Command
     *
     * Generic command function, just like redis-cli
     * @param	string	full command as a string
     * @return 	mixed
     */
    public function command($string)
    {
        $slices = explode(' ', $string);
        $request = $this->_encode_request($slices[0], array_slice($slices, 1));
        return $this->_write_request($request);
    }
    /**
     * Auth
     *
     * Runs the AUTH command when password is set
     * @param 	string	password for the Redis server
     * @return 	void
     */
    private function _auth($password = NULL)
    {
        // Authenticate when password is set
        if ( ! empty($password))
        {
            // See if we authenticated successfully
            if ($this->command('AUTH ' . $password) !== 'OK')
            {
                show_error('Could not connect to Redis, invalid password');
            }
        }
    }
    /**
     * Clear Socket
     *
     * Empty the socket buffer of theconnection so data does not bleed over
     * to the next message.
     * @return 	NULL
     */
    public function _clear_socket()
    {
        // Read one character at a time
        fflush($this->_connection);
        return NULL;
    }
    /**
     * Write request
     *
     * Write the formatted request to the socket
     * @param	string 	request to be written
     * @return 	mixed
     */
    private function _write_request($request)
    {
        if ($this->debug === TRUE)
        {
            log_message('debug', 'Redis unified request: ' . $request);
        }
        // How long is the data we are sending?
        $value_length = strlen($request);
        // If there isn't any data, just return
        if ($value_length <= 0) return NULL;
        // Handle reply if data is less than or equal to 8192 bytes, just send it over
        if ($value_length <= 8192)
        {
            fwrite($this->_connection, $request);
        }
        else
        {
            while ($value_length > 0)
            {
                // If we have more than 8192, only take what we can handle
                if ($value_length > 8192) {
                    $send_size = 8192;
                }
                // Send our chunk
                fwrite($this->_connection, $request, $send_size);
                // How much is left to send?
                $value_length = $value_length - $send_size;
                // Remove data sent from outgoing data
                $request = substr($request, $send_size, $value_length);
            }
        }
        // Read our request into a variable
        $return = $this->_read_request();
        // Clear the socket so no data remains in the buffer
        $this->_clear_socket();
        return $return;
    }
    /**
     * Read request
     *
     * Route each response to the appropriate interpreter
     * @return 	mixed
     */
    private function _read_request()
    {
        $type = fgetc($this->_connection);
        // Times we will attempt to trash bad data in search of a
        // valid type indicator
        $response_types = array('+', '-', ':', '$', '*');
        $type_error_limit = 50;
        $try = 0;
        while ( ! in_array($type, $response_types) && $try < $type_error_limit)
        {
            $type = fgetc($this->_connection);
            $try++;
        }
        if ($this->debug === TRUE)
        {
            log_message('debug', 'Redis response type: ' . $type);
        }
        switch ($type)
        {
            case '+':
                return $this->_single_line_reply();
                break;
            case '-':
                return $this->_error_reply();
                break;
            case ':':
                return $this->_integer_reply();
                break;
            case '$':
                return $this->_bulk_reply();
                break;
            case '*':
                return $this->_multi_bulk_reply();
                break;
            default:
                return FALSE;
        }
    }
    /**
     * Single line reply
     *
     * Reads the reply before the EOF
     * @return 	mixed
     */
    private function _single_line_reply()
    {
        $value = rtrim(fgets($this->_connection));
        $this->_clear_socket();
        return $value;
    }
    /**
     * Error reply
     *
     * Write error to log and return false
     * @return 	bool
     */
    private function _error_reply()
    {
        // Extract the error message
        $error = substr(rtrim(fgets($this->_connection)), 4);
        log_message('error', 'Redis server returned an error: ' . $error);
        $this->_clear_socket();
        return FALSE;
    }
    /**
     * Integer reply
     *
     * Returns an integer reply
     * @return 	int
     */
    private function _integer_reply()
    {
        return (int) rtrim(fgets($this->_connection));
    }
    /**
     * Bulk reply
     *
     * Reads to amount of bits to be read and returns value within
     * the pointer and the ending delimiter
     * @return  string
     */
    private function _bulk_reply()
    {
        // How long is the data we are reading? Support waiting for data to
        // fully return from redis and enter into socket.
        $value_length = (int) fgets($this->_connection);
        if ($value_length <= 0) return NULL;
        $response = '';
        // Handle reply if data is less than or equal to 8192 bytes, just read it
        if ($value_length <= 8192)
        {
            $response = fread($this->_connection, $value_length);
        }
        else
        {
            $data_left = $value_length;
            // If the data left is greater than 0, keep reading
            while ($data_left > 0 ) {
                // If we have more than 8192, only take what we can handle
                if ($data_left > 8192)
                {
                    $read_size = 8192;
                }
                else
                {
                    $read_size = $data_left;
                }
                // Read our chunk
                $chunk = fread($this->_connection, $read_size);
                // Support reading very long responses that don't come through
                // in one fread
                $chunk_length = strlen($chunk);
                while ($chunk_length < $read_size)
                {
                    $keep_reading = $read_size - $chunk_length;
                    $chunk .= fread($this->_connection, $keep_reading);
                    $chunk_length = strlen($chunk);
                }
                $response .= $chunk;
                // Re-calculate how much data is left to read
                $data_left = $data_left - $read_size;
            }
        }
        // Clear the socket in case anything remains in there
        $this->_clear_socket();
        return isset($response) ? $response : FALSE;
    }
    /**
     * Multi bulk reply
     *
     * Reads n bulk replies and return them as an array
     * @return 	array
     */
    private function _multi_bulk_reply()
    {
        // Get the amount of values in the response
        $response = array();
        $total_values = (int) fgets($this->_connection);
        // Loop all values and add them to the response array
        for ($i = 0; $i < $total_values; $i++)
        {
            // Remove the new line and carriage return before reading
            // another bulk reply
            fgets($this->_connection, 2);
            // If this is a second or later pass, we also need to get rid
            // of the $ indicating a new bulk reply and its length.
            if ($i > 0)
            {
                fgets($this->_connection);
                fgets($this->_connection, 2);
            }
            $response[] = $this->_bulk_reply();
        }
        // Clear the socket
        $this->_clear_socket();
        return isset($response) ? $response : FALSE;
    }
    /**
     * Encode request
     *
     * Encode plain-text request to Redis protocol format
     * @link 	http://redis.io/topics/protocol
     * @param 	string 	request in plain-text
     * @param   string  additional data (string or array, depending on the request)
     * @return 	string 	encoded according to Redis protocol
     */
    private function _encode_request($method, $arguments = array())
    {
        $request = '$' . strlen($method) . self::CRLF . $method . self::CRLF;
        $_args = 1;
        // Append all the arguments in the request string
        foreach ($arguments as $argument)
        {
            if (is_array($argument))
            {
                foreach ($argument as $key => $value)
                {
                    // Prepend the key if we're dealing with a hash
                    if (!is_int($key))
                    {
                        $request .= '$' . strlen($key) . self::CRLF . $key . self::CRLF;
                        $_args++;
                    }
                    $request .= '$' . strlen($value) . self::CRLF . $value . self::CRLF;
                    $_args++;
                }
            }
            else
            {
                $request .= '$' . strlen($argument) . self::CRLF . $argument . self::CRLF;
                $_args++;
            }
        }
        $request = '*' . $_args . self::CRLF . $request;
        return $request;
    }
    /**
     * Info
     *
     * Overrides the default Redis response, so we can return a nice array
     * of the server info instead of a nasty string.
     * @return 	array
     */
    public function info($section = FALSE)
    {
        if ($section !== FALSE)
        {
            $response = $this->command('INFO '. $section);
        }
        else
        {
            $response = $this->command('INFO');
        }
        $data = array();
        $lines = explode(self::CRLF, $response);
        // Extract the key and value
        foreach ($lines as $line)
        {
            $parts = explode(':', $line);
            if (isset($parts[1])) $data[$parts[0]] = $parts[1];
        }
        return $data;
    }
    /**
     * Debug
     *
     * Set debug mode
     * @param	bool 	set the debug mode on or off
     * @return 	void
     */
    public function debug($bool)
    {
        $this->debug = (bool) $bool;
    }
    /**
     * Destructor
     *
     * Kill the connection
     * @return 	void
     */
    function __destruct()
    {
        if ($this->_connection) fclose($this->_connection);
    }
}